@file:OptIn(ExperimentalJsCollectionsApi::class)

package io.github.xyzboom.cj

import DebugAdapterTracker
import DebugAdapterTrackerFactory
import DebugSession
import Thenable
import debug.registerDebugAdapterTrackerFactory
import io.github.xyzboom.cj.CjTextDocProvider.delayRefreshOnUri
import io.github.xyzboom.cj.CjTextDocProvider.uriForEval
import io.github.xyzboom.cj.data.DebugExpression
import io.github.xyzboom.cj.data.DebugVariable
import io.github.xyzboom.cj.data.EvalNameAsIdDebugVariable
import io.github.xyzboom.cj.data.IdOnlyVariable
import io.github.xyzboom.cj.data.VarId
import io.github.xyzboom.cj.data.cleanCurrentData
import io.github.xyzboom.cj.data.currentDebugData
import io.github.xyzboom.cj.data.currentDebugSession
import io.github.xyzboom.cj.data.debugData
import io.github.xyzboom.cj.decl.CjEvalResultBody
import io.github.xyzboom.cj.decl.CjVarResultBody
import io.github.xyzboom.cj.decl.result
import io.github.xyzboom.cj.decl.type
import io.github.xyzboom.cj.decl.variables
import io.github.xyzboom.cj.decl.variablesReference
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await
import kotlinx.coroutines.promise
import vscode.languages.setTextDocumentLanguage
import vscode.ExtensionContext
import vscode.Range
import vscode.TextDocumentShowOptions
import vscode.TextEditor
import vscode.commands.registerCommand
import vscode.window.showInformationMessage
import vscode.window.showInputBox
import vscode.window.showTextDocument
import vsocde.workspace.openTextDocument
import vsocde.workspace.registerTextDocumentContentProvider
import kotlin.js.collections.toList

suspend fun getUserInput(): String? {
    val arg = js("{}")
    arg.placeHolder = "Please enter the expr you want evaluate"
    return showInputBox(arg).await()
}

fun DebugSession.sendEvaluate(
    expr: String, extraArgBuilder: MutableMap<String, Any?>.() -> Unit = {}
): Thenable<Any> {
    val arg = js("{}")
    arg.expression = expr
    val extraArg = buildMap(extraArgBuilder)
    for ((key, value) in extraArg) {
        arg[key] = value
    }
    return customRequest("evaluate", arg)
}

/**
 * 4 position request will be sent. so [extraArgBuilder] will be called 4 times
 */
@Suppress("DuplicatedCode")
fun DebugSession.sendCheckRange(
    expr: String, extraArgBuilder: MutableMap<String, Any?>.() -> Unit = {}
) {
    sendEvaluate("${expr}${beginLineProperty}") {
        put(keyCheckPos, true)
        put(keyCheckBeginLine, true)
        extraArgBuilder()
    }
    sendEvaluate("${expr}${beginColumnProperty}") {
        put(keyCheckPos, true)
        put(keyCheckBeginColumn, true)
        extraArgBuilder()
    }
    sendEvaluate("${expr}${endLineProperty}") {
        put(keyCheckPos, true)
        put(keyCheckEndLine, true)
        extraArgBuilder()
    }
    sendEvaluate("${expr}${endColumnProperty}") {
        put(keyCheckPos, true)
        put(keyCheckEndColumn, true)
        extraArgBuilder()
    }
}

fun DebugSession.sendLookupVariables(
    varId: VarId, extraArgBuilder: MutableMap<String, Any?>.() -> Unit = {}
): Thenable<Any> {
    devPrintln("sendLookupVariables: $varId")
    if (varId.isNull) return null.asDynamic()
    val arg = js("{}")
    arg.variablesReference = varId.value.toIntOrNull() ?: return null.asDynamic()
    val extraArg = buildMap(extraArgBuilder)
    for ((key, value) in extraArg) {
        arg[key] = value
    }
    return customRequest("variables", arg)
}

@OptIn(DelicateCoroutinesApi::class)
fun cjVisual(it: Any): Any {
    devPrintln("cj.ast.visual: " + JSON.stringify(it))
    val session: DebugSession = currentDebugSession ?: run {
        devPrintln("no activeDebugSession")
        showInformationMessage("Not In Debug Mode!")
        return@cjVisual Unit
    }
    val visualArg = it.asDynamic()
    if (visualArg == undefined) {
        val debugData = session.debugData
        GlobalScope.promise {
            val input = getUserInput()
            input ?: return@promise
            val debugExpr = debugData.exprMap[input]
            if (debugExpr != null) {
                if (debugExpr.willNeverReady) {
                    showInformationMessage("$input is not a Cangjie AST")
                    return@promise
                }
                if (debugExpr.ready) {
                    showCjAstVisualEditor(debugExpr)
                } else {
                    session.sendEvaluate(input) {
                        put(key_cj_visual_eval, true)
                    }
                }
            } else {
                session.sendEvaluate(input) {
                    put(key_cj_visual_eval, true)
                }
            }
        }
        return Unit
    }
    val varId = VarId(visualArg.variable.variablesReference as Int)
    showInformationMessage("Hello World!")
    val data = currentDebugData!!
    data.variables[varId] ?: session.sendLookupVariables(varId)
    return Unit
}

fun cjVisualAllVar(it: Any): Any {
    if (!devMode) {
        devPrintln("dev mode not enabled!")
        return Unit
    }
    val debugData = currentDebugData ?: run {
        devPrintln("not in debug mode")
        return Unit
    }
    devPrintln(debugData.variables.size)
    devPrintln(debugData.variables.entries.joinToString("\n"))
    return Unit
}

val programSteppedCmd = listOf("stepIn", "continue", "next", "stepOut")

fun onWillReceiveMessage(arg: dynamic) {
    devPrintln("rev: " + JSON.stringify(arg))
    val debugData = currentDebugData ?: return
    val innerArgs = arg.arguments
    if (innerArgs != null && innerArgs != undefined) {
        val seq = arg.seq
        if (innerArgs[key_cj_visual_eval]) {
            debugData.evalSeqIds[seq] = innerArgs.expression
        }
        val id = innerArgs[keyEvalCheckPos]
        if (id) {
            debugData.evalCheckPosSeqIds[seq] = id
        }
        if (innerArgs[keyCheckEndLine]) {
            debugData.checkEndLineSeqIds.add(seq)
        }
        if (innerArgs[keyCheckBeginLine]) {
            debugData.checkBeginLineSeqIds.add(seq)
        }
        if (innerArgs[keyCheckEndColumn]) {
            debugData.checkEndColumnSeqIds.add(seq)
        }
        if (innerArgs[keyCheckBeginColumn]) {
            debugData.checkBeginColumnSeqIds.add(seq)
        }
    }
    val cmd: String = arg.command
    if (cmd in programSteppedCmd) {
        devPrintln("step, clear debug data")
        cleanCurrentData()
    }
    if (cmd == "variables") {
        debugData.varSeqIds[arg.seq] = innerArgs.variablesReference
    }
}

@OptIn(DelicateCoroutinesApi::class)
fun showCjAstVisualEditor(expr: DebugExpression) {
    GlobalScope.promise {
        val uri = uriForEval(expr.evalStr)
        val doc = openTextDocument(uri).await()
        setTextDocumentLanguage(doc, "Cangjie").await()
        showTextDocument(doc, object : TextDocumentShowOptions {
            override var preview: Boolean? = false
            override var selection: Range? = Range(
                expr.beginLine!! - 1, 0, expr.beginLine!! - 1, 0
            )
        }).await()
    }
}

@OptIn(DelicateCoroutinesApi::class)
fun onDidSendMessage(arg: dynamic) {
    devPrintln("send: " + JSON.stringify(arg))
    val debugData = currentDebugData ?: return
    val session = debugData.session
    val requestId = arg.request_seq ?: return
    when (requestId) {
        in debugData.evalSeqIds -> {
            val expr = debugData.evalSeqIds[requestId]!!
            val body: CjEvalResultBody = arg.body
            val type = body.type
            if (type == undefined || !type.startsWith("std.ast")) {
                val message = if (type == undefined) {
                    "reason: ${arg.message}"
                } else {
                    "actual type: $type"
                }
                showInformationMessage("$expr is not a Cangjie AST\n$message")
                return
            }
            devPrintln(JSON.stringify(body))
            val exprId = body.variablesReference
            val debugExpr = DebugExpression(exprId, type, expr)
            debugData.exprMap[expr] = debugExpr
            debugData.exprIdMap[exprId] = debugExpr
            session.sendCheckRange(expr) {
                put(keyEvalCheckPos, exprId)
            }
            debugData.session.sendLookupVariables(VarId(exprId)) {
                debugData.lookupChildrenVar[exprId] = expr to exprId
            }
            devPrintln("send lookup $exprId")
            debugData.evalSeqIds.remove(requestId)
        }

        in debugData.varSeqIds -> {
            devPrintln("debugData.lookupChildrenVar" + debugData.lookupChildrenVar)
            val varId = debugData.varSeqIds[requestId]!!
            val pair = debugData.lookupChildrenVar.remove(varId)
            devPrintln("found $requestId in varSeq, varId is $varId")
            debugData.varSeqIds.remove(requestId)
            val debugVar = if (varId == 0) {
                if (pair == null) {
                    return
                }
                val parentVar = debugData.variables[VarId(pair.second)] ?: return
                EvalNameAsIdDebugVariable(parentVar.variable)
            } else {
                debugData.variables[VarId(varId)] ?: DebugVariable(IdOnlyVariable(varId))
            }
            val body: CjVarResultBody = arg.body
            val variables = body.variables.toList()
            for (cjVariable in variables) {
                val childVar = if (cjVariable.variablesReference != 0) {
                    DebugVariable(cjVariable)
                } else {
                    EvalNameAsIdDebugVariable(cjVariable)
                }
                val childId = childVar.id
                debugVar.childrenIds.add(childId)
                devPrintln("found child $childId")
                debugData.variables[childId] = childVar
                if (pair != null) {
                    debugData.session.sendLookupVariables(childId) {
                        if (childId.isNull) {
                            return@sendLookupVariables
                        }
                        debugData.lookupChildrenVar[childId.value.toInt()] = pair.first to varId
                    }
                }
            }
            debugData.variables[VarId(varId)] = debugVar
            if (pair != null && variables.isEmpty()) {
                delayRefreshOnUri(uriForEval(pair.first))
            }
        }

        in debugData.evalCheckPosSeqIds -> {
            val exprId = debugData.evalCheckPosSeqIds[requestId]
            devPrintln("expr id: $exprId")
            val body: CjEvalResultBody = arg.body
            val expr = debugData.exprIdMap[exprId] ?: return
            // willNeverReady reported by others, terminate
            if (expr.willNeverReady) {
                return
            }
            val type = body.type
            if (type != "Int32") {
                expr.willNeverReady = true
                val message = if (type == undefined) {
                    "reason: ${arg.message}"
                } else {
                    "actual type: $type"
                }
                showInformationMessage("${expr.evalStr} is not a Cangjie AST\n$message")
                return
            }
            val pos = body.result.toInt()
            debugData.setExprPosition(expr, requestId, pos)
            if (expr.ready) {
                showCjAstVisualEditor(expr)
            }
        }
    }
}

fun onWillStopSession() {
    devPrintln("debug session stop, clear debug data")
    cleanCurrentData()
}

fun cjVisualRefresh(it: Any): Any {
    val editor: TextEditor? = vscode.window.activeTextEditor
    if (editor == null) {
        return Unit // no editor
    }
    val document = editor.document
    if (document.uri.scheme !== cj_visual_scheme) {
        return Unit // not my scheme
    }
    CjTextDocProvider.emitter.fire(document.uri)
    return Unit
}

@OptIn(ExperimentalJsCollectionsApi::class)
fun activate(context: ExtensionContext) {
    devPrintln("Activating Cangjie AST Visualization")
    val textDocProvider = registerTextDocumentContentProvider(cj_visual_scheme, CjTextDocProvider)
    context.subscriptions.asDynamic().push(textDocProvider)
    val cmd = registerCommand("cj.ast.visual", ::cjVisual)
    context.subscriptions.asDynamic().push(cmd)
    val cmdRefresh = registerCommand("cj.ast.visual.refresh", ::cjVisualRefresh)
    context.subscriptions.asDynamic().push(cmdRefresh)
    val cmdAllVar = registerCommand("cj.ast.visual.allVar", ::cjVisualAllVar)
    context.subscriptions.asDynamic().push(cmdAllVar)
    val debugAdapter = registerDebugAdapterTrackerFactory("*", object : DebugAdapterTrackerFactory {
        override fun createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker {
            return object : DebugAdapterTracker {
                override val onWillStartSession: (() -> Unit)?
                    get() = null
                override val onWillReceiveMessage: (Any) -> Unit
                    get() = ::onWillReceiveMessage
                override val onDidSendMessage: ((Any) -> Unit)?
                    get() = ::onDidSendMessage
                override val onWillStopSession: (() -> Unit)?
                    get() = ::onWillStopSession
                override val onError: ((Error) -> Unit)?
                    get() = null
                override val onExit: ((Number?, String?) -> Unit)?
                    get() = null

            }
        }
    })
    context.subscriptions.asDynamic().push(debugAdapter)
}